package com.sz.biz.logistics.ord.service;

import com.sz.biz.common.utils.TrimString;
import com.sz.biz.logistics.constants.OrderNumberConstants.RuleRegex;
import com.sz.biz.logistics.constants.OrderNumberConstants.WaybillNumber;
import com.sz.biz.logistics.ord.enums.OrdServiceTypeEnum;
import com.sz.common.base.cache.redis.RedisPoolManager;
import com.sz.common.base.utils.CollectionUtils;
import com.sz.common.base.utils.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

import static com.sz.biz.common.utils.TrimString.countZero;

/**
 * Package: com.sz.biz.logistics.ord.service<br>
 * Function: 生成订单编号<br>
 * @author WuHan<br>
 * @date 2017-08-29 10:51:00<br>
 * @since sys-1.1.1.3
 * @version 1.0
 * @serial
 */
@Component
public class WaybillNumberGenerator implements Serializable {

    private static final int TEN_MINS = 1000 * 60 * 10;
    private static OrdWaybillNoManagementService
            ordWaybillNoManagementService;
    
    private static OrdWaybillNoPrearrangeService
            ordWaybillNoPrearrangeService;
    
    /**
     * 含参构造器，用于避免引用静态方法时引发的NullPointerExceptions.
     * @param ordWaybillNoManagementService 订单管理服务接口
     * @param ordWaybillNoPrearrangeService 订单预分配服务接口
     * */
    @Autowired
    public WaybillNumberGenerator(OrdWaybillNoManagementService ordWaybillNoManagementService,
            OrdWaybillNoPrearrangeService ordWaybillNoPrearrangeService
    ) {
        WaybillNumberGenerator.ordWaybillNoManagementService =
                ordWaybillNoManagementService;
        WaybillNumberGenerator.ordWaybillNoPrearrangeService =
                ordWaybillNoPrearrangeService;
    }
    
    private static RLock lock;
    
    private static Redisson redisson;
    
    static {
        redisson = RedisPoolManager.getRedisson();
        lock = redisson.getFairLock("RedisLock");
    }
    
    /**
     * 校验位生成（按位加权求和加位数取余(mod 10)算法，若某位为0，则该位数值按（位置序号（0开始）+ 串长 - 串中0的个数）参与计算）
     * @param pattern 传入的模式串，作为生成校验位的母串
     * @param length 模式串长度，作为合理性判据
     * @param regex 模式串形式正则，作为合理性判据
     * @param weight 按模式串位数存储的权值数组
     * @return [0,9]之间的整数
     * @throws IllegalArgumentException 参数不合理
     * @throws RuntimeException 数组长度有误（权值数组长度小于模式串长度）
     * */
    private static String getVerifyCode(String pattern,
            int length,
            String regex,
            int[] weight
    )
    throws RuntimeException {
        int sum = 0;
        try {
            int[] singleDigit = new int[pattern.length()];
            for (int i = 0; i < pattern.length(); i++) {
                singleDigit[i] = pattern.charAt(i) - '0' == 0
                        ? (i + length - countZero(pattern)) % 10
                        : pattern.charAt(i) - '0';
                sum += singleDigit[i] * weight[i];
            }
            pattern = TrimString.fillStringLengthByZero(String.valueOf(Long.valueOf(pattern) + 1L), 8);
        } catch (Exception e) {
            if (pattern.length() != length) {
                throw new IllegalArgumentException("L",e);
            } else if (!Pattern.matches(regex, pattern)) {
                throw new IllegalArgumentException("F",e);
            } else if (weight.length < length) {
                throw new IllegalArgumentException("S",e);
            }
            switch (e.getMessage()) {
                case "F":
                    System.out.println("模式串须为纯数字！");
                    break;
                case "L":
                    System.out.println("模式串长度有误！");
                    break;
                case "S":
                    System.out.println("权重数组长度不足！");
                    break;
                default:
                    break;
            }
        }
        return Integer.toString((sum + length) % 10).substring(0,1);
    }
    
    /**
     * 给定辅助条件的校验方法
     * @param pattern 不包含校验位和业务类型的8位序号。
     * @see #getVerifyCode(String, int, String, int[])
     * */
    private static String getVerifyCode(String pattern) throws Exception {
        int[] weight = {8,1,6,3,4,5,2,7};
        return getVerifyCode(pattern, 8, RuleRegex.WAYBILLNUMBER_REGEX, weight);
    }
    
    /**
     * 根据传入的参数生成唯一的订单编号。
     * @param ordServiceTypeEnum 业务类型枚举。
     * @return 10位的订单编号，形式为：1位业务类型+8位基础编号+1位校验。
     * @throws Exception 参见{@link #getVerifyCode(String, int, String, int[])}。
     * */
    public static String generateWaybillNumber(OrdServiceTypeEnum ordServiceTypeEnum)
    throws Exception {
        String serialNumber = "";
        try {
            if (lock.tryLock(100, 30, TimeUnit.SECONDS)) {
                RMap<String, String> waybillNoMap = redisson.getMap("waybillNoMap");
        
                if (CollectionUtils.isEmpty(waybillNoMap) || StringUtils.isEmpty(waybillNoMap.get(WaybillNumber.LAST_SYCHRONIZATION_TIME)) || requireSync(waybillNoMap.get(WaybillNumber.LAST_SYCHRONIZATION_TIME))) {
                    sqlWaybillNoToRedis();
                    waybillNoMap = redisson.getMap("waybillNoMap");
                }
        
                switch (ordServiceTypeEnum) {
                    case Logistics:
                        serialNumber = rMapOperations(waybillNoMap, WaybillNumber.LOGISTIC_HEAD_NUMBER, WaybillNumber.LOGISTIC_NEAREST_MARGIN, WaybillNumber.LOGISTIC_NEAREST_AVAILABLE_NUMBER);
                        break;
                    case Trading:
                        serialNumber = rMapOperations(waybillNoMap, WaybillNumber.TRADING_HEAD_NUMBER, WaybillNumber.TRADING_NEAREST_MARGIN, WaybillNumber.TRADING_NEAREST_AVAILABLE_NUMBER);
                        break;
                    case Finance:
                        serialNumber = rMapOperations(waybillNoMap, WaybillNumber.FINANCE_HEAD_NUMBER, WaybillNumber.FINANCE_NEAREST_MARGIN, WaybillNumber.FINANCE_NEAREST_AVAILABLE_NUMBER);
                        break;
                    default:
                        throw new IllegalArgumentException("传入业务类型不正确！");
                }
                
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        
        String verifyCode = getVerifyCode(serialNumber);
        ordWaybillNoManagementService
                .save(ordServiceTypeEnum.getType(), serialNumber, verifyCode);
        return ordServiceTypeEnum.getType() + serialNumber + verifyCode;
    }
    
    /**
     * 对RMap对象进行操作。
     * @param rMap 要操作的RMap对象。
     * @param headNumber 所需业务类型当前索引。
     * @param nearestMargin 所需业务类型最近的已分配号段起点。
     * @param nearestAvailableNumber 所需业务类型最近的未分配号段起点。
     * @return 待求序列号。
     * */
    private static String rMapOperations(RMap<String, String> rMap, String headNumber, String nearestMargin, String nearestAvailableNumber) {
        String serialNumber = String.valueOf(Long.valueOf(rMap.get(headNumber)) + 1);
        
        if (serialNumber.equals(rMap.get(nearestMargin))) {
            serialNumber = rMap.get(nearestAvailableNumber);
        }
        if (serialNumber == null
                || serialNumber.isEmpty()
                || "".equals(serialNumber)) {
            throw new IllegalArgumentException("传入序号为空！");
        }
    
        serialNumber = TrimString.fillStringLengthByZero(serialNumber, 8);
    
        if (!serialNumber.matches(RuleRegex.WAYBILLNUMBER_REGEX)) {
            throw new IllegalArgumentException("序号长度不合理或不为纯数字串！");
        }
        rMap.put(headNumber, serialNumber);
        return serialNumber;
    }
    
    /**
     * 根据传入的参数生成唯一的订单编号，为供应商下单预留。
     * @param serviceType 业务类型。
     * @param serialNumber 序列编号。
     * @return 10位的订单编号，形式为：1位业务类型+8位基础编号+1位校验。
     * @throws Exception 参见{@link #getVerifyCode(String, int, String, int[])}。
     * */
    public static String generateWaybillNumber(String serviceType, String serialNumber) throws Exception {
        try {
            if (serialNumber == null
                    || serialNumber.isEmpty()
                    || "".equals(serialNumber)) {
                throw new IllegalArgumentException("传入序号为空！");
            }
    
            if (!serialNumber.matches(RuleRegex.WAYBILLNUMBER_REGEX)) {
                throw new IllegalArgumentException("序号长度不合理或不为纯数字串！");
            }
    
            if (!serviceType.equals(OrdServiceTypeEnum.Logistics.getType())
                    && !serviceType.equals(OrdServiceTypeEnum.Trading.getType())
                    && !serviceType.equals(OrdServiceTypeEnum.Finance.getType())) {
                throw new IllegalArgumentException("传入业务类型不正确！");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        String verifyCode = getVerifyCode(serialNumber);
        ordWaybillNoManagementService
                .save(serviceType, serialNumber, verifyCode, true, true, false);
        return serviceType + serialNumber + verifyCode;
    }
    
    /**
     * 将数据库中的单号条目写入Redis
     * 写入以下类型：
     * 1、物流 2、贸易 3、金融各自的：
     * 当前最大已使用非预分配单号，作为起始值。
     * 距起始值最近的预分配号段起始值，用作碰撞检测。
     * 距起始值最近的下一个未分配号段起始值，用作发生碰撞时重新定位起始值
     * */
    public static void sqlWaybillNoToRedis() throws InterruptedException {
        List<String> topNumbers = ordWaybillNoManagementService
                .fetchTopNumbers();
        List<String> nearestMargins = ordWaybillNoPrearrangeService
                .fetchNearestMargins(topNumbers);
        List<String> availableNumbers = ordWaybillNoPrearrangeService
                .fetchAvailableNumbers(topNumbers);
        try {
            if (lock.tryLock(30, 5, TimeUnit.SECONDS)) {
                RMap<String, String> waybillMap = redisson.getMap("waybillNoMap");
                waybillMap.put(WaybillNumber.LOGISTIC_HEAD_NUMBER,
                               redisCacheForwardUpdate(waybillMap.get(WaybillNumber.LOGISTIC_HEAD_NUMBER),
                                                       topNumbers.get(0)));
                waybillMap.put(WaybillNumber.TRADING_HEAD_NUMBER,
                               redisCacheForwardUpdate(waybillMap.get(WaybillNumber.TRADING_HEAD_NUMBER),
                                                       topNumbers.get(1)));
                waybillMap.put(WaybillNumber.FINANCE_HEAD_NUMBER,
                               redisCacheForwardUpdate(waybillMap.get(WaybillNumber.FINANCE_HEAD_NUMBER),
                                                       topNumbers.get(2)));
                waybillMap.put(WaybillNumber.LOGISTIC_NEAREST_MARGIN,
                               redisCacheForwardUpdate(waybillMap.get(WaybillNumber.LOGISTIC_NEAREST_MARGIN),
                                                       nearestMargins.get(0)));
                waybillMap.put(WaybillNumber.TRADING_NEAREST_MARGIN,
                               redisCacheForwardUpdate(waybillMap.get(WaybillNumber.TRADING_NEAREST_MARGIN),
                                                       nearestMargins.get(1)));
                waybillMap.put(WaybillNumber.FINANCE_NEAREST_MARGIN,
                               redisCacheForwardUpdate(waybillMap.get(WaybillNumber.FINANCE_NEAREST_MARGIN),
                                                       nearestMargins.get(2)));
                waybillMap.put(WaybillNumber.LOGISTIC_NEAREST_AVAILABLE_NUMBER,
                               redisCacheForwardUpdate(waybillMap.get(WaybillNumber.LOGISTIC_NEAREST_AVAILABLE_NUMBER),
                                                       availableNumbers.get(0)));
                waybillMap.put(WaybillNumber.TRADING_NEAREST_AVAILABLE_NUMBER,
                               redisCacheForwardUpdate(waybillMap.get(WaybillNumber.TRADING_NEAREST_AVAILABLE_NUMBER),
                                                       availableNumbers.get(1)));
                waybillMap.put(WaybillNumber.FINANCE_NEAREST_AVAILABLE_NUMBER,
                               redisCacheForwardUpdate(waybillMap.get(WaybillNumber.FINANCE_NEAREST_AVAILABLE_NUMBER),
                                                       availableNumbers.get(2)));
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
                waybillMap.put(WaybillNumber.LAST_SYCHRONIZATION_TIME, sdf.format(new Date()));
            }
        } finally {
            lock.unlock();
        }
    }
    
    /**
     * 判断Redis缓存中的数据是否需要与数据库同步。若超过10分钟则需要同步
     * @param lastSyncTime 自Redis中取出的最近一次同步时间。
     * @return 是否需要同步 <br>1.true - 需要同步<br>2.false - 不需要同步
     * */
    private static boolean requireSync(String lastSyncTime) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        boolean res = false;
        try {
            long syncTime = sdf.parse(lastSyncTime).getTime();
            if (System.currentTimeMillis() - syncTime >= TEN_MINS) {
                res = true;
            }
        } catch (Exception e) {
            if (e.getClass().equals(ParseException.class)) {
                e.printStackTrace();
                System.out.println("传入日期字串格式不正确或内容无法解析！");
            }
        }
        return res;
    }
    
    private static String redisCacheForwardUpdate(String originalValue, String updateValue) {
        if (!StringUtils.isEmpty(originalValue) && Long.valueOf(originalValue) >= Long.valueOf(updateValue)) {
            return originalValue;
        }
        return updateValue;
    }
}
